var mxIsElectron = navigator.userAgent != null &&
    navigator.userAgent.toLowerCase().indexOf(' electron/') > -1;
var GOOGLE_APPS_MAX_AREA = 25000000;
var GOOGLE_SHEET_MAX_AREA = 1048576; //1024x1024

//TODO Add support for loading math from a local folder
Editor.initMath((remoteMath ? 'https://app.diagrams.net/' : '') + 'math/MathJax.js');

function render(data) {
    var autoScale = false;

    if (data.scale == 'auto') {
        autoScale = true;
        data.scale = 1;
    }

    document.body.innerHTML = '';
    var container = document.createElement('div');
    container.id = 'graph';
    container.style.width = '100%';
    container.style.height = '100%';
    document.body.appendChild(container);

    var graph = new Graph(container);
    data.border = parseInt(data.border) || 0;
    data.w = parseFloat(data.w) || 0;
    data.h = parseFloat(data.h) || 0;
    data.scale = parseFloat(data.scale) || 1;

    var extras = null;

    try {
        extras = JSON.parse(data.extras);
    } catch (e) {
        try {
            extras = JSON.parse(decodeURIComponent(data.extras));
        } catch (e) {
            // ignore
        }
    }

    var gridColor = null;

    if (extras != null && extras.grid != null) {
        graph.gridSize = extras.grid.size;
        graph.view.gridSteps = extras.grid.steps;
        gridColor = extras.grid.color;
    }

    if (extras != null && extras.diagramLanguage != null) {
        Graph.diagramLanguage = extras.diagramLanguage;
        Graph.translateDiagram = true;
    }

    //PNG+XML format
    if (data.xml.substring(0, 5) == 'iVBOR' || (extras != null && extras.isPng)) {
        data.xml = Editor.extractGraphModelFromPng('data:image/png;base64,' + data.xml);
    }

    //IE11 sends incorrect xml
    if (data.xml.substring(0, 11) == '<#document>') {
        data.xml = data.xml.substring(11, data.xml.length - 12);
    }

    // Parses XML
    var doc = mxUtils.parseXml(data.xml);
    var node = Editor.extractGraphModel(doc.documentElement, true);

    if (node == null) {
        //Electron pdf export
        try {
            const {ipcRenderer} = require('electron');

            ipcRenderer.send('render-finished', null);
        } catch (e) {
            console.log(e);
        }

        return graph;
    }

    var xmlDoc = node.ownerDocument;
    var diagrams = null;
    var from = 0;

    if (mxIsElectron && data.format == 'xml') {
        const {ipcRenderer} = require('electron');

        try {
            var xml = mxUtils.getXml(xmlDoc);
            EditorUi.prototype.createUi = function () {
            };
            EditorUi.prototype.addTrees = function () {
            };
            EditorUi.prototype.updateActionStates = function () {
            };
            var editorUi = new EditorUi();
            var tmpFile = new LocalFile(editorUi, xml);
            editorUi.setCurrentFile(tmpFile);
            editorUi.setFileData(xml);
            ipcRenderer.send('xml-data', mxUtils.getXml(editorUi.getXmlFileData(null, null, data.uncompressed)));
        } catch (e) {
            ipcRenderer.send('xml-data-error');
        }

        return;
    }

    // Handles mxfile
    if (xmlDoc.documentElement.nodeName == 'mxfile') {
        diagrams = xmlDoc.documentElement.getElementsByTagName('diagram');
    }

    //Add global variables to graph
    if (extras != null && extras.globalVars != null) {
        graph.globalVars = extras.globalVars;
    }

    /**
     * Disables custom links on shapes.
     */
    var graphGetLinkForCell = graph.getLinkForCell;

    graph.getLinkForCell = function (cell) {
        var link = graphGetLinkForCell.apply(this, arguments);

        if (link != null && this.isCustomLink(link)) {
            link = null;
        }

        return link;
    };

    /**
     * Disables custom links in labels.
     */
    var cellRendererRedrawLabelShape = graph.cellRenderer.redrawLabelShape;

    graph.cellRenderer.redrawLabelShape = function (shape) {
        cellRendererRedrawLabelShape.apply(this, arguments);

        if (shape.node != null) {
            var links = shape.node.getElementsByTagName('a');

            for (var i = 0; i < links.length; i++) {
                var href = links[i].getAttribute('href');

                if (href != null && graph.isCustomLink(href)) {
                    links[i].setAttribute('href', '#');
                }
            }
        }
    };

    var preview = null;
    var waitCounter = 1;
    var bounds;
    var pageId;
    var expScale;
    // Waits for all images to finish loading
    var cache = new Object();
    var math = false;

    // Decrements waitCounter and invokes callback when finished
    function decrementWaitCounter() {
        if (--waitCounter < 1) {
            //Note: This code targets Chrome as it is the browser used by export server
            //Ensure that all fonts has been loaded, this promise is never rejected
            document.fonts.ready.then(function () {
                var doneDiv = document.createElement("div");
                var pageCount = diagrams != null ? diagrams.length : 1;
                doneDiv.id = 'LoadingComplete';
                doneDiv.style.display = 'none';
                doneDiv.setAttribute('bounds', JSON.stringify(bounds));
                doneDiv.setAttribute('page-id', pageId);
                doneDiv.setAttribute('scale', expScale);
                doneDiv.setAttribute('pageCount', pageCount);
                document.body.appendChild(doneDiv);

                //Electron pdf export
                if (mxIsElectron) {
                    try {
                        const {ipcRenderer} = require('electron');

                        ipcRenderer.on('get-svg-data', (event, arg) => {
                            graph.mathEnabled = math; //Enable math such that getSvg works as expected
                            // Returns the exported SVG for the given graph (see EditorUi.exportSvg)
                            var bg = graph.background;

                            if (bg == mxConstants.NONE) {
                                bg = null;
                            }

                            var svgRoot = graph.getSvg(bg, 1, 0, false, null, true, null, null, null);

                            if (graph.shadowVisible) {
                                graph.addSvgShadow(svgRoot);
                            }

                            // TODO addFontCss cannot be used as it requires this
                            // Adds CSS
                            //Editor.prototype.addFontCss(svgRoot);

                            if (math) {
                                Editor.prototype.addMathCss(svgRoot);
                            }

                            ipcRenderer.send('svg-data', '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
                                mxUtils.getXml(svgRoot));
                        });

                        //For some reason, Electron 9 doesn't send this object as is without stringifying. Usually when variable is external to function own scope
                        ipcRenderer.send('render-finished', {bounds: JSON.stringify(bounds), pageCount: pageCount});
                    } catch (e) {
                        console.log(e);
                    }
                }
            });
        }
    };

    function waitForImages(tagName, attributeName) {
        var imgs = document.body.getElementsByTagName(tagName);
        waitCounter += imgs.length;

        for (var i = 0; i < imgs.length; i++) {
            // No load events for image elements in Phantom using indirection instead
            var src = imgs[i].getAttribute(attributeName);

            if (src != null && src.length > 0 && cache[src] == null) {
                cache[src] = new Image();
                cache[src].onload = decrementWaitCounter;
                cache[src].onerror = decrementWaitCounter;
                cache[src].src = src;
            } else {
                decrementWaitCounter();
            }
        }
    };

    // Waits for MathJax.Hub to become available to register
    // wait counter callback asynchronously after math render
    var editorDoMathJaxRender = Editor.doMathJaxRender;

    Editor.doMathJaxRender = function (container) {
        editorDoMathJaxRender.apply(this, arguments);

        window.setTimeout(function () {
            window.MathJax.Hub.Queue(function () {
                decrementWaitCounter();
            });
        }, 0);
    };

    // Adds async MathJax rendering task
    function renderMath(elt) {
        if (math && Editor.MathJaxRender != null) {
            waitCounter++;
            Editor.MathJaxRender(elt);
        }
    };

    function loadExtFonts(extFonts) {
        try {
            extFonts = extFonts.split('|').map(function (ef) {
                var parts = ef.split('^');
                return {name: parts[0], url: parts[1]};
            });
        } catch (e) {
            //ignore and return!
            return;
        }

        waitCounter += extFonts.length;

        //Note: This code targets Chrome as it is the browser used by export server
        for (var i = 0; i < extFonts.length; i++) {
            if (extFonts[i].url.indexOf(Editor.GOOGLE_FONTS) == 0) {
                var link = document.createElement('link');

                link.setAttribute('rel', 'stylesheet');
                link.setAttribute('charset', 'UTF-8');
                link.setAttribute('type', 'text/css');

                link.onload = decrementWaitCounter;
                link.onerror = decrementWaitCounter;

                link.setAttribute('href', extFonts[i].url);
                var head = document.getElementsByTagName('head')[0];
                head.appendChild(link);
            } else {
                //Relative urls doesn't work
                if (extFonts[i].url.indexOf(PROXY_URL) == 0 && PROXY_URL.indexOf('http') == -1) {
                    var href = window.location.href;
                    href = href.substring(0, href.lastIndexOf('/') + 1);
                    extFonts[i].url = href + extFonts[i].url;
                }

                var font = new FontFace(extFonts[i].name, 'url(' + extFonts[i].url + ')');

                font.load().then(function (loadedFont) {
                    document.fonts.add(loadedFont);
                    decrementWaitCounter();
                }).catch(decrementWaitCounter);
            }
        }
    };

    function renderGrid() {
        if (gridColor == null) return;

        var view = graph.view;
        var gridImage = btoa(unescape(encodeURIComponent(view.createSvgGrid(gridColor))));
        gridImage = 'url(' + 'data:image/svg+xml;base64,' + gridImage + ')';
        var phase = graph.gridSize * view.gridSteps * view.scale;

        var x0 = 0;
        var y0 = 0;

        if (view.backgroundPageShape != null) {
            var bds = view.getBackgroundPageBounds();

            x0 = 1 + bds.x;
            y0 = 1 + bds.y;
        }

        // Computes the offset to maintain origin for grid
        var position = -Math.round(phase - mxUtils.mod(view.translate.x * view.scale - x0, phase)) + 'px ' +
            -Math.round(phase - mxUtils.mod(view.translate.y * view.scale - y0, phase)) + 'px';

        var pages = document.querySelectorAll('[id^=mxPage]');

        var cssTxt = 'margin: 0;padding: 0;background-image: ' + gridImage + ';background-position: ' + position;
        document.body.style.cssText = cssTxt;

        for (var i = 0; i < pages.length; i++) {
            pages[i].style.cssText = cssTxt;
        }
    };

    var origAddFont = Graph.addFont;

    Graph.addFont = function (name, url) {
        waitCounter++;
        return origAddFont.call(this, name, url, decrementWaitCounter);
    };

    function renderPage() {
        // Enables math typesetting
        math |= xmlDoc.documentElement.getAttribute('math') == '1';

        //Load external fonts
        var extFonts = xmlDoc.documentElement.getAttribute('extFonts');

        if (extFonts) {
            loadExtFonts(extFonts);
        }

        // Configure graph
        graph.foldingEnabled = false;
        graph.setEnabled(false);

        // Sets background image
        var bgImg = xmlDoc.documentElement.getAttribute('backgroundImage');

        if (bgImg != null) {
            bgImg = JSON.parse(bgImg);
            graph.setBackgroundImage(new mxImage(bgImg.src, bgImg.width, bgImg.height));
        }

        // Parses XML into graph
        var codec = new mxCodec(xmlDoc);
        var model = graph.getModel();
        codec.decode(xmlDoc.documentElement, model);

        var bg;

        if (data.format == 'pdf') {
            if (data.bg == 'none') {
                bg = null;
            } else {
                bg = xmlDoc.documentElement.getAttribute('background');

                if (bg == 'none' || !bg) {
                    bg = '#ffffff';
                }
            }
        } else {
            // Loads background color
            bg = (data.bg != null && data.bg.length > 0) ?
                data.bg : xmlDoc.documentElement.getAttribute('background');

            // Normalizes values for transparent backgrounds
            if (bg == 'none' || bg == '') {
                bg = null;
            }

            // Checks if export format supports transparent backgrounds
            if (bg == null && data.format != 'gif' && data.format != 'png') {
                bg = '#ffffff';
            }
        }

        // Sets background color on page
        if (bg != null) {
            document.body.style.backgroundColor = bg;
        }

        //handle layers
        if (extras != null && ((extras.layers != null && extras.layers.length > 0) ||
            (extras.layerIds != null && extras.layerIds.length > 0))) {
            var childCount = model.getChildCount(model.root);

            // Hides all layers
            for (var i = 0; i < childCount; i++) {
                model.setVisible(model.getChildAt(model.root, i), false);
            }

            if (extras.layerIds != null) {
                for (var i = 0; i < extras.layerIds.length; i++) {
                    model.setVisible(model.getCell(extras.layerIds[i]), true);
                }
            } else {
                for (var i = 0; i < extras.layers.length; i++) {
                    var layer = model.getChildAt(model.root, extras.layers[i]);

                    if (layer != null) {
                        model.setVisible(layer, true);
                    }
                }
            }
        }

        // Sets initial value for PDF page background
        graph.pdfPageVisible = false;

        // Handles PDF output where the output should match the page format if the page is visible
        if (data.print || (data.format == 'pdf' && xmlDoc.documentElement.getAttribute('page') == '1' && data.w == 0 && data.h == 0 && data.scale == 1)) {
            //Electron printing
            var printScale = 1;

            if (data.print) {
                document.title = data.fileTitle;

                var gb = graph.getGraphBounds();
                printScale = data.pageScale;

                if (isNaN(printScale)) {
                    printScale = 1;
                }

                if (data.fit) {
                    var h = parseInt(data.sheetsAcross);
                    var v = parseInt(data.sheetsDown);

                    data.scale = Math.min((data.pageHeight * v) / (gb.height / graph.view.scale),
                        (data.pageWidth * h) / (gb.width / graph.view.scale));
                } else {
                    data.scale = data.scale / graph.pageScale;

                    if (isNaN(data.scale)) {
                        printScale = 1 / graph.pageScale;
                    }
                }
            }

            var pw = data.pageWidth || xmlDoc.documentElement.getAttribute('pageWidth');
            var ph = data.pageHeight || xmlDoc.documentElement.getAttribute('pageHeight');
            graph.pdfPageVisible = true;

            if (pw != null && ph != null) {
                graph.pageFormat = new mxRectangle(0, 0, parseFloat(pw), parseFloat(ph));
            }

            var ps = data.pageScale || xmlDoc.documentElement.getAttribute('pageScale');

            if (ps != null) {
                graph.pageScale = ps;
            }

            graph.getPageSize = function () {
                return new mxRectangle(0, 0, this.pageFormat.width * this.pageScale,
                    this.pageFormat.height * this.pageScale);
            };

            graph.getPageLayout = function () {
                var size = this.getPageSize();
                var bounds = this.getGraphBounds();

                if (bounds.width == 0 || bounds.height == 0) {
                    return new mxRectangle(0, 0, 1, 1);
                } else {
                    // Computes untransformed graph bounds
                    var x = Math.ceil(bounds.x / this.view.scale - this.view.translate.x);
                    var y = Math.ceil(bounds.y / this.view.scale - this.view.translate.y);
                    var w = Math.floor(bounds.width / this.view.scale);
                    var h = Math.floor(bounds.height / this.view.scale);

                    var x0 = Math.floor(x / size.width);
                    var y0 = Math.floor(y / size.height);
                    var w0 = Math.ceil((x + w) / size.width) - x0;
                    var h0 = Math.ceil((y + h) / size.height) - y0;

                    return new mxRectangle(x0, y0, w0, h0);
                }
            };

            // Fits the number of background pages to the graph
            graph.view.getBackgroundPageBounds = function () {
                var layout = this.graph.getPageLayout();
                var page = this.graph.getPageSize();

                return new mxRectangle(this.scale * (this.translate.x + layout.x * page.width),
                    this.scale * (this.translate.y + layout.y * page.height),
                    this.scale * layout.width * page.width,
                    this.scale * layout.height * page.height);
            };
        }

        if (!graph.pdfPageVisible) {
            var b = graph.getGraphBounds();

            // Floor is needed to keep rendering crisp
            if (data.w > 0 || data.h > 0) {
                var s = 1;

                if (data.w > 0 && data.h > 0) {
                    s = Math.min(data.w / b.width, data.h / b.height);
                } else if (data.w > 0) {
                    s = data.w / b.width;
                } else {
                    s = data.h / b.height;
                }

                graph.view.scaleAndTranslate(s,
                    Math.floor(data.border / s - Math.floor(b.x)),
                    Math.floor(data.border / s - Math.floor(b.y)));
            } else {
                var s = data.scale;

                if (autoScale) {
                    var pageWidth = (extras != null && extras.pageWidth != null) ? extras.pageWidth : 800;

                    if (b.width < pageWidth & b.height < 1.5 * pageWidth) {
                        s = 4;
                    } else if (b.width < 2 * pageWidth & b.height < 3 * pageWidth) {
                        s = 3;
                    } else if (b.width < 4 * pageWidth && b.height < 6 * pageWidth) {
                        s = 2;
                    }

                    if (extras != null && extras.isGoogleSheet != null) {
                        GOOGLE_APPS_MAX_AREA = GOOGLE_SHEET_MAX_AREA;
                    }

                    //The image cannot exceed 25 MP to be included in Google Apps
                    if (b.width * s * b.height * s > GOOGLE_APPS_MAX_AREA) {
                        //Subtracting 0.01 to prevent any other rounding that can make slightly over 25 MP
                        s = Math.sqrt(GOOGLE_APPS_MAX_AREA / (b.width * b.height)) - 0.01;
                    }
                }

                graph.view.scaleAndTranslate(s,
                    Math.floor(data.border - Math.floor(b.x)),
                    Math.floor(data.border - Math.floor(b.y)));
            }
        } else {
            // Disables border for PDF page export
            data.border = 0;

            // Moves to first page in page layout
            var layout = graph.getPageLayout();
            var page = graph.getPageSize();
            var dx = layout.x * page.width;
            var dy = layout.y * page.height;

            if (dx != 0 || dy != 0) {
                graph.view.setTranslate(Math.floor(-dx), Math.floor(-dy));
            }
        }

        // Gets the diagram bounds and sets the document size
        bounds = (graph.pdfPageVisible) ? graph.view.getBackgroundPageBounds() : graph.getGraphBounds();
        bounds.width = Math.ceil(bounds.width + data.border) + 1; //The 1 extra pixels to prevent cutting the cells on the edges when crop is enabled
        bounds.height = Math.ceil(bounds.height + data.border);
        expScale = graph.view.scale || 1;

        // Converts the graph to a vertical sequence of pages for PDF export
        if (graph.pdfPageVisible) {
            var pf = graph.pageFormat || mxConstants.PAGE_FORMAT_A4_PORTRAIT;
            var scale = data.print ? data.scale : 1 / graph.pageScale;
            var autoOrigin = (data.print && data.fit != null) ? data.fit : false;
            var border = 0;

            // Negative coordinates are cropped or shifted if page visible
            var gb = graph.getGraphBounds();
            var x0 = 0;
            var y0 = 0;

            // Applies print scale
            pf = mxRectangle.fromRectangle(pf);
            pf.width = Math.ceil(pf.width * printScale) + 1; //The 1 extra pixels to prevent cutting the cells on the right edge of the page
            pf.height = Math.ceil(pf.height * printScale);
            scale *= printScale;

            // Starts at first visible page
            if (!autoOrigin) {
                var layout = graph.getPageLayout();
                x0 -= layout.x * pf.width;
                y0 -= layout.y * pf.height;
            }

            if (preview == null) {
                preview = new mxPrintPreview(graph, scale, pf, border, x0, y0);
                preview.printBackgroundImage = true;
                preview.autoOrigin = autoOrigin;
                preview.backgroundColor = bg;
                // Renders print output into this document and removes the graph container
                preview.open(null, window);
                graph.container.parentNode.removeChild(graph.container);
            } else {
                preview.backgroundColor = bg;
                preview.autoOrigin = autoOrigin;
                preview.appendGraph(graph, scale, x0, y0);
            }
            // Adds shadow
            // NOTE: Shadow rasterizes output
            /*if (mxClient.IS_SVG && xmlDoc.documentElement.getAttribute('shadow') == '1')
            {
                var svgs = document.getElementsByTagName('svg');

                for (var i = 0; i < svgs.length; i++)
                {
                    var svg = svgs[i];

                    var filter = graph.addSvgShadow(svg, null, true);
                    filter.setAttribute('id', 'shadow-' + i);
                    svg.appendChild(filter);
                    svg.setAttribute('filter', 'url(#' + 'shadow-' + i + ')');
                }

                border = 7;
            }*/

            bounds = new mxRectangle(0, 0, pf.width, pf.height);
        } else {
            // Adds shadow
            // NOTE: PDF shadow rasterizes output so it's disabled
            if (data.format != 'pdf' && mxClient.IS_SVG && xmlDoc.documentElement.getAttribute('shadow') == '1') {
                graph.addSvgShadow(graph.view.canvas.ownerSVGElement, null, true);
                graph.setShadowVisible(true);
                bounds.width += 7;
                bounds.height += 7;
            }

            document.body.style.width = Math.ceil(bounds.x + bounds.width) + 'px';
            document.body.style.height = Math.ceil(bounds.y + bounds.height) + 'px';
        }
    }

    if (diagrams != null && diagrams.length > 0) {
        var to = diagrams.length - 1;

        //Parameters to and all pages should not be sent with formats other than PDF with page view enabled
        if (!data.allPages) {
            if (data.pageId != null) {
                for (var i = 0; i < diagrams.length; i++) {
                    if (data.pageId == diagrams[i].getAttribute('id')) {
                        from = i;
                        to = i;
                        break;
                    }
                }
            } else {
                from = Math.max(0, Math.min(parseInt(data.from) || from, diagrams.length - 1));
                to = parseInt(data.to);
                //If to is not defined, use from (so one page), otherwise, to is restricted to the range from "from" to diagrams.length - 1
                to = isNaN(to) ? from : Math.max(from, Math.min(to, diagrams.length - 1));
            }
        }

        /**
         * Implements %page% and %pagenumber% placeholders
         */
        var graphGetGlobalVariable = graph.getGlobalVariable;

        graph.getGlobalVariable = function (name) {
            if (name == 'page') {
                return (diagrams == null) ? 'Page-1' :
                    (diagrams[from].getAttribute('name') || ('Page-' + (from + 1)));
            } else if (name == 'pagenumber') {
                return from + 1;
            }

            return graphGetGlobalVariable.apply(this, arguments);
        };

        for (var i = from; i <= to; i++) {
            if (diagrams[i] != null) {
                if (pageId == null) {
                    pageId = diagrams[i].getAttribute('id')
                }

                xmlDoc = Editor.parseDiagramNode(diagrams[i]);

                if (xmlDoc != null) {
                    xmlDoc = xmlDoc.ownerDocument;
                }

                graph.getModel().clear();
                from = i;
                renderPage();
            }
        }
    } else {
        renderPage();
    }

    if (fallbackFont) {
        //Add a fallbackFont font to all labels in case the selected font doesn't support the character
        //Some systems doesn't have a good fallback fomt that supports all languages
        //Use this with a custom font-face in export-fonts.css file
        document.querySelectorAll('foreignObject div').forEach(d => d.style.fontFamily = (d.style.fontFamily || '') + ', ' + fallbackFont);
    }

    renderGrid();
    // Includes images in SVG and HTML labels
    waitForImages('image', 'xlink:href');
    waitForImages('img', 'src');
    renderMath(document.body);
    // Immediate return if not waiting for any content
    decrementWaitCounter();

    return graph;
};

//Electron pdf export
if (mxIsElectron) {
    try {
        const {ipcRenderer} = require('electron');

        ipcRenderer.on('render', (event, arg) => {
            render(arg);
        });
    } catch (e) {
        console.log(e);
    }
}
